﻿Imports System.Net
Imports System.Net.sockets
Imports System.Threading

Public Class ClientSocket

    Protected _socket As Socket
    Protected _buffer() As Byte
    Protected _thread As Thread
    Protected _isReceiving As Boolean
    Protected _isDisconnected As Boolean = True
    Protected _isFreeTime As Boolean = False
    Protected _lastTicks As Long
    Protected _freeTicks As Long
    Protected _heartData() As Byte
    Protected _heartIdleTime As Integer               '心跳包接受发送时间
    Protected _heartWaitTime As Integer               '心跳包接受等待服务器响应间隔
    Protected _heartRep As Integer                    '等待服务器次数
    Protected _heartDataClient() As Byte = Text.Encoding.ASCII.GetBytes("Hearting") '客户端心跳包数据
    Protected _heartDataServer() As Byte = _heartDataClient '服务端心跳包数据
    Protected _receiveBufferSize As Integer = 512
    Protected _sendBufferSize As Integer = 512
    Protected _receiveTimeout As Integer = 0
    Protected _sendTimeout As Integer = 0
    Protected _encoding As Text.Encoding = Text.Encoding.ASCII
    'Protected _receiveLockObject As New Object()
    Protected _isBusy As Boolean = False


    Public Event OnConnect As EventHandler(Of ConnectEventArgs)
    Public Event OnReceive As EventHandler(Of ReceiveEventArgs)
    Public Event OnDisconnect As EventHandler(Of DisconnectArgs)
    Public Event OnFreeTime As EventHandler
    Public Event OnHeartWait As EventHandler(Of HeartWaitArgs)
    Public Event OnHeartTimeout As EventHandler


#Region "Properties"

    ''' <summary>
    ''' 客户端Socket对象
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Socket() As Socket
        Get
            Return _socket
        End Get
    End Property

    ''' <summary>
    ''' 数据接收缓冲区大小(字节)。默认512字节。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property ReceiveBufferSize() As Integer
        Get
            Return _receiveBufferSize
        End Get
        Set(ByVal value As Integer)
            _receiveBufferSize = value
        End Set
    End Property

    ''' <summary>
    ''' 数据发送缓冲区大小(字节)。默认512字节。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property SendBufferSize() As Integer
        Get
            Return _sendBufferSize
        End Get
        Set(ByVal value As Integer)
            _sendBufferSize = value
        End Set
    End Property

    ''' <summary>
    ''' 数据接收超时值(毫秒)。默认值为 0，指示超时期限无限大。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property ReceiveTimeout() As Integer
        Get
            Return _receiveTimeout
        End Get
        Set(ByVal value As Integer)
            _receiveTimeout = value
        End Set
    End Property

    ''' <summary>
    ''' 数据发送超时值(毫秒)。默认值为 0，指示超时期限无限大。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property SendTimeout() As Integer
        Get
            Return _sendTimeout
        End Get
        Set(ByVal value As Integer)
            _sendTimeout = value
        End Set
    End Property

    ''' <summary>
    ''' 心跳包开始发送时间(毫秒)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property HeartIdleTime() As Long
        Get
            Return _heartIdleTime / 10000
        End Get
        Set(ByVal value As Long)
            _heartIdleTime = value * 10000
        End Set
    End Property

    ''' <summary>
    ''' 心跳包发送等待服务器响应间隔(毫秒)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property HeartWaitTime() As Long
        Get
            Return _heartWaitTime
        End Get
        Set(ByVal value As Long)
            _heartWaitTime = value
        End Set
    End Property

    ''' <summary>
    ''' 等待服务器响应次数
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property HeartRep() As Integer
        Get
            Return _heartRep
        End Get
        Set(ByVal value As Integer)
            _heartRep = value
        End Set
    End Property

    ''' <summary>
    ''' 服务器端心跳包数据
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property HeartDataServer() As Byte()
        Get
            Return _heartDataServer
        End Get
        Set(ByVal value As Byte())
            _heartDataServer = value
        End Set
    End Property

    ''' <summary>
    ''' 客户端心跳包数据
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property HeartDataClient() As Byte()
        Get
            Return _heartDataClient
        End Get
        Set(ByVal value As Byte())
            _heartDataClient = value
        End Set
    End Property

    ''' <summary>
    ''' 客户端是否断开
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsDisconnected() As Boolean
        Get
            Return _isDisconnected
        End Get
    End Property

    ''' <summary>
    ''' 当前缓冲区中可用数据长度
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Available() As Integer
        Get
            Return _socket.Available
        End Get
    End Property

    ''' <summary>
    ''' 字符编码方式(默认ASCII)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Encoding() As Text.Encoding
        Get
            Return _encoding
        End Get
        Set(ByVal value As Text.Encoding)
            _encoding = value
        End Set
    End Property

#End Region


    Public Sub New()
    End Sub

    Private Sub InitSocket()
        _socket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
        _socket.ReceiveBufferSize = _receiveBufferSize
        _socket.SendBufferSize = _sendBufferSize
        _socket.ReceiveTimeout = _receiveTimeout
        _socket.SendTimeout = _sendTimeout
    End Sub

    '连接服务器
    Public Function ConnectToServer(ByVal ipAddress As String, ByVal port As Integer) As Boolean
        Try
            Dim ipep As New IPEndPoint(Net.IPAddress.Parse(ipAddress), port)
            InitSocket()

            Debug.Write("ClientSocket.ConnectToServer() Connect " & ipAddress & ":" & port & vbCrLf)
            _socket.Connect(ipep)
            Debug.Write("ClientSocket.ConnectToServer() Connected OK" & vbCrLf)

            If _thread Is Nothing Then
                _thread = New Thread(AddressOf WorkerThread)
                _thread.IsBackground = True
                _isReceiving = True
                _thread.Start()
            End If

            _isDisconnected = False
            _isFreeTime = False
            RaiseEvent OnConnect(Me, New ConnectEventArgs(_socket.RemoteEndPoint))
        Catch ex As SocketException
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.ConnectToServer() Connect Failed" & vbCrLf)
            Return False
        Catch ex As Exception
            Debug.Write("ClientSocket.ConnectToServer() Connect Failed" & vbCrLf)
            Return False
        End Try

        Return True
    End Function

    '数据接收工作线程
    Private Sub WorkerThread()
        Dim bufLen As Integer = 0
        Dim nowTicks As Long = 0
        Dim times As Integer = 0
        _lastTicks = DateTime.Now.Ticks

        Do While _isReceiving
            nowTicks = DateTime.Now.Ticks


            If _isFreeTime Then
                Do While times < _heartRep
                    If _isFreeTime = False OrElse _isBusy = True Then Exit Do
                    times += 1

                    If _isDisconnected = False Then RaiseEvent OnHeartWait(Me, New HeartWaitArgs(times))
                    Debug.Write("ClientSocket.WorkerThread() OnHeartWait " & times & vbCrLf)
                    BeginHeart()
                    If _isDisconnected Then Exit Do

                    'Thread.Sleep(_heartWaitTime)
                Loop

                If _isFreeTime Then
                    times = 0
                    If _isDisconnected = False Then RaiseEvent OnHeartTimeout(Me, EventArgs.Empty)
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.HeartTimeout))
                    Debug.Write("ClientSocket.WorkerThread() OnHeartTimeout" & vbCrLf)
                    Exit Do
                End If
            Else
                '检测是否有数据
                Try
                    If _socket.Poll(0, SelectMode.SelectRead) Then
                        bufLen = _socket.Available
                        If bufLen = 0 Then
                            '检测客户端空闲状态
                            Try
                                If (nowTicks - _lastTicks) > _heartIdleTime Then
                                    If _isFreeTime = False Then
                                        _freeTicks = DateTime.Now.Ticks
                                        _isFreeTime = True
                                        times = 0
                                        'BeginHeart()
                                        If _isDisconnected = False Then RaiseEvent OnFreeTime(Me, EventArgs.Empty)
                                        Debug.Write("ClientSocket.WorkerThread() OnFreeTime" & vbCrLf)
                                    End If
                                Else
                                    _isFreeTime = False
                                End If
                            Catch ex As ObjectDisposedException

                            Catch ex As NullReferenceException

                            Catch ex As Exception
                                'Throw ex
                            End Try
                        Else
                            _isFreeTime = False
                            _lastTicks = DateTime.Now.Ticks
                            If _isDisconnected = False Then RaiseEvent OnReceive(Me, New ReceiveEventArgs(bufLen))
                            'Debug.Write("ClientSocket.WorkerThread() Receive OK" & vbCrLf)
                        End If
                    Else
                        '检测客户端空闲状态
                        Try
                            If (nowTicks - _lastTicks) > _heartIdleTime Then
                                If _isFreeTime = False Then
                                    _freeTicks = DateTime.Now.Ticks
                                    _isFreeTime = True
                                    times = 0
                                    'BeginHeart()
                                    If _isDisconnected = False Then RaiseEvent OnFreeTime(Me, EventArgs.Empty)
                                    Debug.Write("ClientSocket.WorkerThread() OnFreeTime" & vbCrLf)
                                End If
                            Else
                                _isFreeTime = False
                            End If
                        Catch ex As ObjectDisposedException

                        Catch ex As NullReferenceException

                        Catch ex As Exception
                            'Throw ex
                        End Try
                    End If
                Catch ex As ObjectDisposedException
                    Exit Do
                Catch ex As Exception
                    'Throw ex
                End Try
            End If

            Thread.Sleep(500)
        Loop

    End Sub

    '发送数据
    Public Function Send(ByVal bytes() As Byte) As Integer
        If bytes Is Nothing Then Return -1
        Send = 0

        SetNotFreeTime()
        _isBusy = True
        Try

            Send = _socket.Send(bytes, SocketFlags.None)
            'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
            If Send = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                Return -1
            End If
        Catch ex As SocketException
            If ex.SocketErrorCode = SocketError.TimedOut Then
                Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                Return -1
            End If
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
        Catch ex As ObjectDisposedException
            Return -1
        Catch ex As NullReferenceException
            Return -1
        Catch ex As Exception
            Throw ex
        Finally
            'SetNotFreeTime()
            _isBusy = False
        End Try

    End Function
    Public Function Send(ByVal bytes() As Byte, ByVal length As Integer) As Integer
        If bytes Is Nothing Then Return -1
        Send = 0

        SetNotFreeTime()
        _isBusy = True
        Try

            Send = _socket.Send(bytes, length, SocketFlags.None)
            'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
            If Send = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                Return -1
            End If
        Catch ex As SocketException
            If ex.SocketErrorCode = SocketError.TimedOut Then
                Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                Return -1
            End If
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
        Catch ex As ObjectDisposedException
            Return -1
        Catch ex As NullReferenceException
            Return -1
        Catch ex As Exception
            Throw ex
        Finally
            'SetNotFreeTime()
            _isBusy = False
        End Try

    End Function
    Public Function Send(ByVal bytes() As Byte, ByVal offset As Integer, ByVal size As Integer) As Integer
        If bytes Is Nothing Then Return -1
        Send = 0

        SetNotFreeTime()
        _isBusy = True
        Try

            Send = _socket.Send(bytes, offset, size, SocketFlags.None)
            'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
            If Send = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                Return -1
            End If
        Catch ex As SocketException
            If ex.SocketErrorCode = SocketError.TimedOut Then
                Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                Return -1
            End If
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
        Catch ex As ObjectDisposedException
            Return -1
        Catch ex As NullReferenceException
            Return -1
        Catch ex As Exception
            Throw ex
        Finally
            'SetNotFreeTime()
            _isBusy = False
        End Try

    End Function
    Public Function Send(ByVal str As String) As Integer

        Send = 0

        Dim bytes() As Byte
        bytes = _encoding.GetBytes(str)

        SetNotFreeTime()
        _isBusy = True
        Try

            Send = _socket.Send(bytes, bytes.Length, SocketFlags.None)
            'Debug.Write("ClientSocket.Send() Send : " & Send & vbCrLf)
            If Send = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Send() Send Failed" & vbCrLf)
                Return -1
            End If
        Catch ex As SocketException
            If ex.SocketErrorCode = SocketError.TimedOut Then
                Debug.Write("ClientSocket.Send() Send TimeOut" & vbCrLf)
                Return -1
            End If
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Send() Send SocketException" & vbCrLf)
        Catch ex As ObjectDisposedException
            Return -1
        Catch ex As NullReferenceException
            Return -1
        Catch ex As Exception
            Throw ex
        Finally
            'SetNotFreeTime()
            _isBusy = False
        End Try

    End Function

    '接收数据
    Public Function Receive(ByVal length As Integer) As Byte()
        ReDim _buffer(length - 1)

        SetNotFreeTime()
        _isBusy = True
        Try
            If _socket.Receive(_buffer, 0, length, SocketFlags.None) = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Receive() Receive Failed" & vbCrLf)
            End If

        Catch ex As SocketException
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Receive() Receive SocketException" & vbCrLf)
        Catch ex As Exception
            Throw ex
        Finally
            SetNotFreeTime()
            _isBusy = False
        End Try

        Return _buffer
    End Function
    Public Function Receive(ByVal buffer() As Byte, ByVal offset As Integer, ByVal size As Integer) As Integer

        SetNotFreeTime()
        _isBusy = True
        Try
            Receive = _socket.Receive(buffer, offset, size, SocketFlags.None)
            'Debug.Write("ClientSocket.Receive() Receive : " & Receive & vbCrLf)
            If Receive = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Receive() Receive Failed" & vbCrLf)
            End If

        Catch ex As SocketException
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Receive() Receive SocketException" & vbCrLf)
        Catch ex As Exception
            Throw ex
        Finally
            SetNotFreeTime()
            _isBusy = False
        End Try

        Return Receive
    End Function

    '接收缓冲中所有存在的数据
    Public Function ReceiveExisting() As String
        Dim length As Integer = _socket.Available
        If length <= 0 Then Return String.Empty
        Dim _buffer(length - 1) As Byte
        ReceiveExisting = String.Empty

        SetNotFreeTime()
        _isBusy = True
        Try
            If _socket.Receive(_buffer, 0, _buffer.Length, SocketFlags.None) = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.Receive() Receive Failed" & vbCrLf)
            Else
                ReceiveExisting = _encoding.GetString(_buffer)
            End If

        Catch ex As SocketException
            socketException(ex.SocketErrorCode)
            Debug.Write("ClientSocket.Receive() Receive SocketException" & vbCrLf)
        Catch ex As Exception
            Throw ex
        Finally
            SetNotFreeTime()
            _isBusy = False
        End Try

    End Function

    Private Sub SetNotFreeTime()
        _isFreeTime = False
        _lastTicks = DateTime.Now.Ticks
    End Sub


    Protected Sub BeginHeart()
        Dim length As Integer = 0
        Dim beginTicks As Long = Now.Ticks
        Dim nowTicks As Long = Now.Ticks
        ReDim _heartData(_heartDataServer.Length - 1)

        Try
            length = _socket.Send(_heartDataClient, SocketFlags.None)
            Debug.Write("ClientSocket.BeginHeart() Send HeartData " & length & vbCrLf)
            If length = 0 Then
                If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                Debug.Write("ClientSocket.BeginHeart() Send HeartData OnDisconnect" & vbCrLf)
                Return
            End If

            Do While nowTicks - beginTicks < _heartWaitTime * 10000
                Try
                    length = _socket.Receive(_heartData, SocketFlags.None)
                    Debug.Write("ClientSocket.BeginHeart() Receive HeartData " & length & vbCrLf)
                    If length = 0 Then
                        If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
                        Debug.Write("ClientSocket.BeginHeart() Receive HeartData OnDisconnect" & vbCrLf)
                        Return
                    Else
                        _isFreeTime = False
                        _lastTicks = DateTime.Now.Ticks

                        If CheckHeart(_heartData) Then
                            Debug.Write("ClientSocket.BeginHeart() Is HeartData" & vbCrLf)
                        Else
                            _buffer = _heartData
                            If _isDisconnected = False Then RaiseEvent OnReceive(Me, New ReceiveEventArgs(_buffer.Length))
                            Debug.Write("ClientSocket.BeginHeart() Not HeartData: " & Me._encoding.GetString(_buffer) & vbCrLf)
                        End If

                        Exit Do
                    End If
                Catch ex As SocketException
                    socketException(ex.SocketErrorCode)
                End Try

                nowTicks = Now.Ticks
                Thread.Sleep(100)
            Loop
            'length = _socket.Receive(_heartData, SocketFlags.None)
            'If length = 0 Then
            '    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Normal))
            '    Return
            'End If
            
        Catch ex As SocketException
            socketException(ex.SocketErrorCode)
        Catch ex As Exception

        End Try
    End Sub

#Region "Heart Beat Functions"

    Protected Function CheckHeart(ByVal bytes() As Byte) As Boolean
        If bytes.Length = _heartDataServer.Length Then
            For i As Integer = 0 To bytes.Length - 1
                If bytes(i) <> _heartDataServer(i) Then
                    Return False
                End If
            Next i
            Return True
        End If
        Return False
    End Function

#End Region


    '关闭客户端
    Public Sub Close(ByVal timeout As Integer)
        If _isDisconnected Then Return
        Try
            _isDisconnected = True
            _isFreeTime = False

            _isReceiving = False
            If _thread IsNot Nothing Then
                '_thread.Abort()
                _thread = Nothing
            End If

            _socket.Shutdown(SocketShutdown.Both)
            _socket.Close(timeout)
            Debug.Write("ClientSocket.Close()" & vbCrLf)
        Catch ex As ObjectDisposedException

        Catch ex As Exception
            Throw ex
        End Try
    End Sub

    Public Sub ForceClose()
        Try
            _isDisconnected = True
            _isFreeTime = False

            _isReceiving = False
            If _thread IsNot Nothing Then
                '_thread.Abort()
                _thread = Nothing
            End If

            '_socket.Disconnect(False)
            '_socket.Shutdown(SocketShutdown.Both)
            _socket.Close(0)
            Debug.Write("ClientSocket.ForceClose()" & vbCrLf)
        Catch ex As ObjectDisposedException

        Catch ex As Exception
            Throw ex
        End Try
    End Sub

    Protected Sub socketException(ByVal errorCode As SocketError)
        Monitor.Enter(Me)
        Debug.Write("ClientSocket.socketException() : " & errorCode.ToString() & vbCrLf)
        Try
            Select Case errorCode
                Case SocketError.ConnectionReset '远程主机强制关闭, 网线断开
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.ConnectionReset))
                Case SocketError.ConnectionAborted '远程主机Socket终止
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.ConnectionAborted))
                Case SocketError.Shutdown '远程主机关闭连接
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.Shutdown))
                Case SocketError.TimedOut
                    If _isDisconnected = False Then RaiseEvent OnDisconnect(Me, New DisconnectArgs(DisconnectType.TimedOut))
                Case SocketError.ConnectionRefused '远程主机未开始服务
                    _socket.Close()
                    'm_clientSocket.Close() : m_clientSocket = Nothing
                    'm_allDone.Close() : m_allDone = Nothing
                    'RaiseEvent OnConnectionRefused(Me, EventArgs.Empty)
                    ''Throw New AppException(errorCode.ToString())
                Case SocketError.HostUnreachable '网卡禁用
                    'RaiseEvent OnHostUnreachable(Me, EventArgs.Empty)
                Case SocketError.WouldBlock

                Case SocketError.NotSocket

                Case SocketError.AddressAlreadyInUse
                    'Me.Close(0)
                    'Throw New AppException("AddressAlreadyInUse")
                Case SocketError.Interrupted

                Case SocketError.NotConnected

                Case SocketError.NetworkUnreachable

                Case Else
                    Throw New Exception(errorCode.ToString())
            End Select
        Finally
            Monitor.Exit(Me)
        End Try

    End Sub

End Class
